package org.openwebtop.common.aop.aspect;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.openwebtop.common.aop.annotation.Cache;
import org.openwebtop.common.aop.constant.CacheRefreshPeriod;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import com.opensymphony.oscache.base.CacheEntry;
import com.opensymphony.oscache.base.NeedsRefreshException;
import com.opensymphony.oscache.general.GeneralCacheAdministrator;

/**
 * Cache Aspect
 *
 * @author Jaehyeon Nam
 * @Since 2010. 1. 11.
 */
@Aspect
@Order(1)
@Component
public class CacheAspect {
	/**
	 * Logger
	 */
	private final static Log LOG = LogFactory.getLog(CacheAspect.class);
	private GeneralCacheAdministrator cacheAdmin;

	/**
	 * Cache Aspect Method
	 *
	 * @param joinPoint JoinPoint
	 * @return JoinPoint Method Invoke Result
	 * @throws Throwable Throwable
	 */
	@Around("@annotation(org.openwebtop.common.aop.annotation.Cache)")
	public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
		final MethodSignature signature = (MethodSignature)joinPoint.getSignature();

		final Cache cacheAnnotation = signature.getMethod().getAnnotation(Cache.class);

		if (cacheAnnotation != null) {
			final String cacheKey = getCacheKey(signature.getDeclaringTypeName(), signature.getName(), ArrayUtils.toString(joinPoint.getArgs()));

			return doCache(joinPoint, cacheKey, cacheAnnotation.refreshPeriod(), cacheAnnotation.updateError());
		}

		return joinPoint.proceed();
	}

	/**
	 * Cache할 Object의 Key를 생성한다.
	 *
	 * @param packageName Cache 대상 Method의 Full Package Name
	 * @param methodName Cache 대상 Method Name
	 * @param argumentName Cache 대상 Method의 Argument Name(ArrayUtils.toString 사용 추천)
	 * @return Cache Key
	 */
	public String getCacheKey(String packageName, String methodName, String argumentName) {
		return String.format("%s.%s(%s)", packageName, methodName, argumentName);
	}

	/**
	 * JoinPoint의 Method 결과를 Caching
	 *
	 * @param joinPoint JoinPoint
	 * @param key Cache Key
	 * @param cacheRefreshPeriod Cache Refresh Period
	 * @param updateError true - 이전 캐시값 사용, false - 이전 캐시값 사용하지 않음
	 * @return Method Invoke Result
	 * @throws Throwable Throwable
	 */
	private Object doCache(ProceedingJoinPoint joinPoint, String key, CacheRefreshPeriod cacheRefreshPeriod, boolean updateError) throws Throwable {
		Object result = null;

		try {
			result = cacheAdmin.getFromCache(key, CacheEntry.INDEFINITE_EXPIRY, cacheRefreshPeriod.getCrontabExpression());

			if (LOG.isDebugEnabled()) {
				LOG.debug("read from cache : " + key);
			}
		} catch (NeedsRefreshException nre) {
			try {
				result = joinPoint.proceed();
				cacheAdmin.putInCache(key, result);

				if (LOG.isDebugEnabled()) {
					LOG.debug("make cache : " + key);
				}
			} catch (Exception ex) {
				try {
					if (updateError) {
						// 예외발생시 이전 캐시값 사용을 한다면 캐시값으로 부터
						result = nre.getCacheContent();
					} else {
						// 아닌경우 예외 발생
						throw ex;
					}
				} finally {
					cacheAdmin.cancelUpdate(key);

					if (LOG.isDebugEnabled()) {
						LOG.debug("cache update fail read from previous cache : " + key);
					}
				}
			}
		}

		return result;
	}

	public void setCacheAdmin(GeneralCacheAdministrator cacheAdmin) {
		this.cacheAdmin = cacheAdmin;
	}

}
